package it.at7.gemini.api.openapi;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import it.at7.gemini.api.ApiUtility;
import it.at7.gemini.core.FilterContextBuilder;
import it.at7.gemini.core.ModuleBase;
import it.at7.gemini.exceptions.GeminiRuntimeException;
import it.at7.gemini.schema.Entity;
import it.at7.gemini.schema.EntityField;
import it.at7.gemini.schema.FieldType;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.function.Consumer;

import static it.at7.gemini.api.openapi.OpenAPIBuilder.SchemaType.*;
import static it.at7.gemini.core.RecordConverters.GEMINI_UUID_FIELD;

public class OpenAPIBuilder {
    public static String OPENAPI_VERSION = "3.0.2";
    public static String INFO_TITLE = "Gemini";
    public static String INFO_VERSION = "1.0-SNAPSHOT";
    public static String INFO_DESCRIPTION = "Gemini: fastest way to create REST API in Java";

    private final List<Tag> tags = new ArrayList<>();
    private final List<Server> servers = new ArrayList<>();
    private final Map<String, Path> pathsByName = new LinkedHashMap<>();
    private final Map<String, Object> components = new LinkedHashMap<>();

    private final Map<String, String> UNAUTHORIZED_REF = Map.of("$ref", "#/components/responses/Unauthorized");
    private final Map<String, String> DEFAULTERR_REF = Map.of("$ref", "#/components/responses/DefaultError");

    private final boolean moduleTag;
    private final List<Path> RESTentitiesPaths = new ArrayList<>();
    private Info info;

    public OpenAPIBuilder() {
        this(true);
    }


    public OpenAPIBuilder(boolean moduleTag) {
        this.moduleTag = moduleTag;
        this.info = null;
        initComponents();
    }

    private void initComponents() {
        components.put("responses", initResponseComponent());
        components.put("schemas", initSchemasComponent());
        components.put("parameters", initParametersComponent());
    }

    private Object initSchemasComponent() {
        return new LinkedHashMap<String, Schema>();
    }

    private Map<String, Response> initResponseComponent() {
        Map<String, Response> responses = new LinkedHashMap<>();
        responses.put("Unauthorized", makeUnauthorizedResponse());
        responses.put("DefaultError", makeDefaultResponse());
        return responses;
    }

    private Object initParametersComponent() {

        LinkedHashMap<String, Parameter> parameters = new LinkedHashMap<>();

        // UUID parameter
        Parameter uuid = new Parameter();
        uuid.name = "uuid";
        uuid.in = "path";
        uuid.required = true;
        uuid.description = "UUID generated by POST request";
        SchemaProperty uuidSchemaProp = new SchemaProperty();
        uuidSchemaProp.type = "string";
        uuidSchemaProp.format = "uuid";
        uuid.schema = uuidSchemaProp;
        parameters.put("uuid", uuid);

        // Gemini Header Parameter
        /*  --- remove the gemini header from openAPI - need a parameter to enable ? It is better to use the content-type
        Parameter geminiHeader = new Parameter();
        geminiHeader.name = "Gemini";
        geminiHeader.in = "header";
        geminiHeader.required = false;
        geminiHeader.description = "Optional Header to enable Gemini Request/Response feature: \n * `gemini.api` - to enable Meta Data Response ";
        SchemaProperty gemHeaderSchemaProp = new SchemaProperty();
        gemHeaderSchemaProp.type = "string";
        gemHeaderSchemaProp._enum = List.of("gemini.api");
        geminiHeader.schema = gemHeaderSchemaProp;
        parameters.put("GeminiHeader", geminiHeader);
         */

        return parameters;
    }

    private Response makeUnauthorizedResponse() {
        Response response = new Response();
        response.description = "Unauthorized";
        return response;
    }

    private Response makeDefaultResponse() {
        Response response = new Response();
        response.description = "Unexpected error";
        return response;
    }

    public String toJsonString() {
        ObjectMapper objectMapper = new ObjectMapper();
        HashMap<String, Object> rootJson = new LinkedHashMap<>();
        rootJson.put("openapi", OPENAPI_VERSION);
        rootJson.put("info", info == null ? makeInfo() : info);
        rootJson.put("servers", servers);
        rootJson.put("paths", pathsByName);
        rootJson.put("tags", tags);
        rootJson.put("components", components);
        try {
            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            return objectMapper.writeValueAsString(rootJson);
        } catch (JsonProcessingException e) {
            throw new GeminiRuntimeException("Unable to serialize JSON");
        }
    }

    private Map<String, Object> makeInfo() {
        HashMap<String, Object> infoJson = new HashMap<>();
        infoJson.put("title", INFO_TITLE);
        infoJson.put("version", INFO_VERSION);
        infoJson.put("description", INFO_DESCRIPTION);
        return infoJson;
    }

    public void addModulesToTags(List<ModuleBase> orderedModules) {
        for (ModuleBase module : orderedModules) {
            tags.add(Tag.from(module.getName(), String.format("Section for all Entities of Module %s", module.getName())));
        }
    }

    public void handleEntity(Entity entity) {
        // Embedable entities are not exposed and don't need path
        if (!entity.isEmbedable()) {
            String entityName = entity.getName();
            String entityPathName = entityName.toLowerCase();
            tags.add(Tag.from(entityName, String.format("Section for entity %s", entityName)));

            // GET and POST on /entityname
            Path rootEntityPath = new Path();
            rootEntityPath.summary = String.format("%s resource route", entityName);
            rootEntityPath.get = getEntityRootMethod(entity);
            if (!entity.isOneRecord() && !entity.isClosedDomain())
                rootEntityPath.post = postNewEntityMethod(entity);
            if (entity.isOneRecord() && !entity.isClosedDomain())
                rootEntityPath.put = putEntityOneRecord(entity);

            // rootEntityPath.parameters = List.of(Map.of("$ref", "#/components/parameters/GeminiHeader"));
            this.pathsByName.put("/" + entityPathName, rootEntityPath);
            this.RESTentitiesPaths.add(rootEntityPath);

            // GET / on entityname/{uuid} -- always available
            Path uuidEntityPath = new Path();
            uuidEntityPath.summary = String.format("%s resource by UUID root", entityName);
            uuidEntityPath.get = getEntityUUIDandLKMethod(entity, "Get the %s resource by its uuid");

            // PUT not enabled on CLOSED Domains
            if (!entity.isClosedDomain())
                uuidEntityPath.put = putEntityUUIDandLKMethod(entity, "Update any %s resource by its uuid");

            // DELETE not enabled withRecord oneRecordEntity or ClosedDomain
            if (!entity.isOneRecord() && !entity.isClosedDomain())
                uuidEntityPath.delete = deleteEntityUUIDandLKMethod(entity, "Delete any %s resource by its uuid");
            uuidEntityPath.parameters = List.of(
                    Map.of("$ref", "#/components/parameters/uuid")
                    // Map.of("$ref", "#/components/parameters/GeminiHeader")
            );
            this.pathsByName.put("/" + entityPathName + "/{uuid}", uuidEntityPath);
            this.RESTentitiesPaths.add(uuidEntityPath);

            // GET / PUT and DELETE on /entityname/{logicalKey/...}
            Entity.LogicalKey logicalKey = entity.getLogicalKey();
            // NOT Embedable withRecord a LK can be used as reference - we are removing also oneRecord
            if (!logicalKey.isEmpty()) {
                String lkPathString = getEntityLKPath(entity, true);
                Path lkPath = new Path();
                lkPath.summary = String.format("%s resource by Entity Logical Key", entityName);
                lkPath.parameters = (List) getEntityLKParameters(entity, true);
                // lkPath.parameters.add(Map.of("$ref", "#/components/parameters/GeminiHeader"));

                lkPath.get = getEntityUUIDandLKMethod(entity, "Get the %s resource by its Logical Key");

                if (!entity.isClosedDomain()) {
                    lkPath.put = putEntityUUIDandLKMethod(entity, "Update any %s resource by its Logical Key");
                    lkPath.delete = deleteEntityUUIDandLKMethod(entity, "Delete any %s resource by its Logical Key");
                }

                this.pathsByName.put("/" + entityPathName + "/" + lkPathString, lkPath);
                this.RESTentitiesPaths.add(lkPath);
                addComponentSchema(entity, ENTITY_LK);
            }

            // SCHEMA withRecord META only if it is not embedable
            addComponentSchema(entity, ENTITY_WITH_META);
        }

        // SCHEMA all the times... even it is embedable
        addComponentSchema(entity, ENTITY);
    }

    private String getEntityLKPath(Entity entity, boolean isLevel0) {
        List<String> lks = new ArrayList<>();
        entity.getLogicalKey().getLogicalKeyList().forEach(lk -> {
            if (lk.getType().equals(FieldType.ENTITY_REF)) {
                assert lk.getEntityRef() != null;
                lks.add(getEntityLKPath(lk.getEntityRef(), false));
            } else {
                String fieldName = lk.getName().toLowerCase();
                lks.add("{" + lkPathParameterName(entity, isLevel0, fieldName) + "}");
            }
        });
        return String.join("/", lks);
    }

    @NotNull
    private List<Parameter> getEntityLKParameters(Entity entity, boolean isLevel0) {
        List<Parameter> parameters = new ArrayList<>();
        entity.getLogicalKey().getLogicalKeyList().forEach(lk -> {
            if (lk.getType().equals(FieldType.ENTITY_REF)) {
                assert lk.getEntityRef() != null;
                parameters.addAll(getEntityLKParameters(lk.getEntityRef(), false));
            } else {
                Parameter lkP = new Parameter();
                lkP.in = "path";
                lkP.required = true;
                lkP.name = lkPathParameterName(entity, isLevel0, lk.getName().toLowerCase());
                lkP.schema = new SchemaProperty();
                fromFieldToProperty(lk, lkP.schema);
                parameters.add(lkP);
            }
        });
        return parameters;

    }

    private String lkPathParameterName(Entity entity, boolean isLevel0, String fieldName) {
        return isLevel0 ? fieldName : (entity.getName().toLowerCase() + "_" + fieldName);
    }

    private Method getEntityRootMethod(Entity entity) {
        Method method = new Method();
        String summary = entity.isOneRecord() ? "Get the unique resource for %s (single record entity)" : "Get the list from %s resources";
        method.summary = String.format(summary, entity.getName());
        method.tags = getTagsForEntityMethod(entity);

        // 200 response has not components default
        Response response200 = new Response();
        response200.description = "Successful operation";
        response200.content = new LinkedHashMap<>();

        if (entity.isOneRecord()) {
            // response single record entity
            response200JsonEntityRecord(entity, response200);
        } else {

            method.parameters = filterParameters();

            // response list
            response200.content.put(String.format("application/json; %s=%s", ApiUtility.GEMINI_CONTENT_TYPE, ApiUtility.GEMINI_API_META_TYPE),
                    Map.of("schema",
                            Map.of("$ref", String.format("#/components/schemas/%s", entityListWithMetaSchemaName(entity)))));
            response200.content.put("application/json",
                    Map.of("schema",
                            Map.of("type", "array",
                                    "items",
                                    Map.of("$ref", String.format("#/components/schemas/%s", entity.getName())))));
        }
        method.responses.put("200", response200);


        method.responses.put("401", UNAUTHORIZED_REF);
        method.responses.put("default", DEFAULTERR_REF);

        return method;
    }

    private List<Object> filterParameters() {
        List<Object> parameters = new ArrayList<>();

        Parameter search = new Parameter();
        search.name = FilterContextBuilder.SEARCH_PARAMETER;
        search.description = "Search string to filter records using RSQL query";
        search.in = "query";
        search.required = false;
        SchemaProperty type = new SchemaProperty();
        type.type = "string";
        search.schema = type;
        search.allowReserver = true;
        search.examples = new HashMap<>();
        search.examples.put("simple", exampleForSearch("Simple search by field", "fieldName==fieldValue"));
        search.examples.put("query", exampleForSearch("RSQL query search", "(fieldName==fieldValue) or (field2=v2 and field3=v3)"));

        Parameter limit = new Parameter();
        limit.name = FilterContextBuilder.LIMIT_PARAMETER;
        limit.description = "Limit the results of the query";
        limit.in = "query";
        limit.required = false;
        type = new SchemaProperty();
        type.type = "integer";
        type.format = "int64";
        limit.schema = type;

        Parameter start = new Parameter();
        start.name = FilterContextBuilder.START_PARAMETER;
        start.description = "Start results from a certain records";
        start.in = "query";
        start.required = false;
        start.schema = type;

        Parameter orderBy = new Parameter();
        orderBy.name = FilterContextBuilder.ORDER_BY_PARAMETER;
        orderBy.description = "Comma separated fields to order results. (Use - to DESC order)";
        orderBy.in = "query";
        orderBy.required = false;
        type = new SchemaProperty();
        type.type = "string";
        orderBy.schema = type;
        orderBy.allowReserver = false;

        parameters.add(search);
        parameters.add(limit);
        parameters.add(start);
        parameters.add(orderBy);
        return parameters;
    }

    private Object exampleForSearch(String description, String value) {
        return Map.of("value", value,
                "summary", description);
    }

    private Method getEntityUUIDandLKMethod(Entity entity, String summary) {
        Method method = new Method();
        method.summary = String.format(summary, entity.getName());
        uuidLkCommonMethodDesc(entity, method, "Successful operation - return the wanted resource");
        return method;
    }

    private Method deleteEntityUUIDandLKMethod(Entity entity, String summary) {
        Method method = new Method();
        method.summary = String.format(summary, entity.getName());
        uuidLkCommonMethodDesc(entity, method, "Successfull DELETE operation - return the remove resource");
        return method;
    }

    private Method putEntityUUIDandLKMethod(Entity entity, String summary) {
        Method method = new Method();
        method.summary = String.format(summary, entity.getName());
        uuidLkCommonMethodDesc(entity, method, "Successfull UPDATE operation - return the modified resource");
        requestBodyEntitySchema(entity, method);
        return method;
    }

    private Method putEntityOneRecord(Entity entity) {
        Method method = new Method();
        method.summary = String.format("Update the unique %s resource (one record entity)", entity.getName());
        uuidLkCommonMethodDesc(entity, method, "Successfull UPDATE operation - return the modified resource");
        requestBodyEntitySchema(entity, method);
        return method;
    }

    private void requestBodyEntitySchema(Entity entity, Method method) {
        method.requestBody = new RequestBody();
        method.requestBody.required = true;
        method.requestBody.content = new LinkedHashMap<>();
        method.requestBody.content.put("application/json",
                Map.of("schema",
                        Map.of("$ref", String.format("#/components/schemas/%s", entity.getName()))));
    }

    @NotNull
    private void uuidLkCommonMethodDesc(Entity entity, Method method, String s) {
        method.tags = getTagsForEntityMethod(entity);

        Response response200 = new Response();
        response200.description = s;
        response200.content = new LinkedHashMap<>();
        response200JsonEntityRecord(entity, response200);
        method.responses.put("200", response200);
        method.responses.put("401", UNAUTHORIZED_REF);
        method.responses.put("default", DEFAULTERR_REF);
    }

    @NotNull
    private void response200JsonEntityRecord(Entity entity, Response response) {
        response.content.put(String.format("application/json; %s=%s", ApiUtility.GEMINI_CONTENT_TYPE, ApiUtility.GEMINI_API_META_TYPE),
                Map.of("schema",
                        Map.of("$ref", String.format("#/components/schemas/%s", entityWithMetaSchemaName(entity)))));

        response.content.put("application/json",
                Map.of("schema",
                        Map.of("$ref", String.format("#/components/schemas/%s", entity.getName()))));
    }

    private Method postNewEntityMethod(Entity entity) {
        Method method = new Method();
        method.summary = String.format("Create a new %s resource", entity.getName());
        method.tags = getTagsForEntityMethod(entity);
        requestBodyEntitySchema(entity, method);
        // 200 response has not components default
        Response response200 = new Response();
        response200.description = "Resource created";
        response200.content = new LinkedHashMap<>();
        response200JsonEntityRecord(entity, response200);
        method.responses.put("200", response200);
        method.responses.put("401", UNAUTHORIZED_REF);
        method.responses.put("default", DEFAULTERR_REF);
        return method;
    }

    @NotNull
    private List<String> getTagsForEntityMethod(Entity entity) {
        if (moduleTag)
            return List.of(entity.getModule().getName(), entity.getName());
        return List.of(entity.getName());
    }

    public void addServer(String url, String description) {
        this.servers.add(Server.from(url, description));
    }


    public void addComponentSchema(Entity entity, SchemaType schemaType) {
        Map<String, Schema> schemas = (Map<String, Schema>) this.components.get("schemas");

        if (schemaType.equals(ENTITY)) {
            Schema entitySchema = createSchemaFor(entity.getDataEntityFields());
            schemas.put(entity.getName(), entitySchema);
        }
        if (schemaType.equals(ENTITY_LK)) {
            Schema entitySchema = createSchemaFor(entity.getLogicalKey().getLogicalKeySet());
            schemas.put(entityLkSchemaName(entity), entitySchema);
        }
        if (!entity.isEmbedable()) {
            if (schemaType.equals(ENTITY_WITH_META)) {
                Schema rootSchema = new Schema();
                rootSchema.type = "object";
                rootSchema.properties = new LinkedHashMap<>();
                Schema metaSchema = createSchemaFor(entity.getMetaEntityFields());

                SchemaProperty uuidProperty = new SchemaProperty();
                uuidProperty.type = "string";
                uuidProperty.format = "uuid";
                metaSchema.properties.put(GEMINI_UUID_FIELD, uuidProperty);

                rootSchema.properties.put("meta", metaSchema);
                Schema dataSchema = createSchemaFor(entity.getDataEntityFields());
                rootSchema.properties.put("data", dataSchema);
                rootSchema.required = List.of("meta", "data");
                schemas.put(entityWithMetaSchemaName(entity), rootSchema);

                Schema listSchema = new Schema();
                listSchema.type = "object";
                listSchema.properties = new LinkedHashMap<>();
                listSchema.properties.put("meta", schemaForFilter());
                listSchema.properties.put("data", Map.of("type", "array",
                        "items", Map.of("$ref", String.format("#/components/schemas/%s", entityWithMetaSchemaName(entity)))));
                listSchema.required = List.of("meta", "data");
                schemas.put(entityListWithMetaSchemaName(entity), listSchema);

            }
        }
    }

    private Object schemaForFilter() {
        Schema schema = new Schema();
        schema.type = "object";
        schema.properties = new LinkedHashMap<>();
        SchemaProperty intProperty = new SchemaProperty();
        intProperty.type = "integer";
        intProperty.format = "int64";

        SchemaProperty orderProperty = new SchemaProperty();
        orderProperty.type = "array";
        orderProperty.items = new SchemaProperty();
        orderProperty.items.type = "string";

        schema.properties.put("limit", intProperty);
        schema.properties.put("start", intProperty);
        schema.properties.put("orderBy", orderProperty);
        return schema;
    }

    private Schema createSchemaFor(Set<EntityField> fields) {
        Schema schema = new Schema();
        schema.type = "object";
        schema.properties = new LinkedHashMap<>();
        List<String> requiredFields = new ArrayList<>();
        for (EntityField field : fields) {
            if (field.isLogicalKey())
                requiredFields.add(field.getName().toLowerCase());
            SchemaProperty schemaProperty = new SchemaProperty();
            String name = field.getName().toLowerCase();
            fromFieldToProperty(field, schemaProperty);
            schema.properties.put(name, schemaProperty);
        }
        if (!requiredFields.isEmpty()) {
            schema.required = requiredFields;
        }
        return schema;
    }

    private void fromFieldToProperty(EntityField field, SchemaProperty schemaProperty) {
        switch (field.getType()) {
            case TEXT:
                schemaProperty.type = "string";
                break;
            case NUMBER:
                schemaProperty.type = "number";
                break;
            case LONG:
                schemaProperty.type = "integer";
                schemaProperty.format = "int64";
                break;
            case DOUBLE:
                schemaProperty.type = "number";
                schemaProperty.format = "double";
                break;
            case BOOL:
                schemaProperty.type = "boolean";
                break;
            case TIME:
                schemaProperty.type = "string";
                schemaProperty.format = "time";
                break;
            case DATE:
                schemaProperty.type = "string";
                schemaProperty.format = "date";
                break;
            case DATETIME:
                schemaProperty.type = "string";
                schemaProperty.format = "date-time";
                break;
            case PASSWORD:
                schemaProperty.type = "string";
                schemaProperty.format = "password";
                break;
            case ENTITY_REF:
                schemaProperty.$ref = String.format("#/components/schemas/%s", entityLkSchemaName(field.getEntityRef()));
                break;
            case ENTITY_EMBEDED:
                schemaProperty.$ref = String.format("#/components/schemas/%s", field.getEntityRef().getName());
                break;
            case GENERIC_ENTITY_REF:
                // TODO
                break;
            case TEXT_ARRAY:
                schemaProperty.type = "array";
                schemaProperty.items = new SchemaProperty();
                schemaProperty.items.type = "string";
                break;
            case ENTITY_REF_ARRAY:
                schemaProperty.type = "array";
                schemaProperty.items = new SchemaProperty();
                schemaProperty.items.type = "object";
                break;
            case RECORD:
                // TODO
                break;
        }
    }


    private String entityLkSchemaName(Entity entity) {
        return entity.getName() + "_LK";
    }

    private String entityWithMetaSchemaName(Entity entity) {
        return entity.getName() + "_META";
    }

    private String entityListWithMetaSchemaName(Entity entity) {
        return "LIST_" + entity.getName() + "_META";
    }

    public void addSecurityComponent(String name, SecuritySchema securitySchema) {
        Map<String, SecuritySchema> securitySchemes = (Map<String, SecuritySchema>) this.components.computeIfAbsent("securitySchemes", k -> new HashMap<String, SecuritySchema>());
        securitySchemes.put(name, securitySchema);
    }

    public void secureAllEntityPaths(String securitySchemaName) {
        Map<String, SecuritySchema> securitySchemes = (Map<String, SecuritySchema>) this.components.get("securitySchemes");
        if (securitySchemes == null || !securitySchemes.containsKey(securitySchemaName)) {
            throw new GeminiRuntimeException(String.format("Security schema %s not found", securitySchemaName));
        }
        Consumer<Method> secureLambda = m -> {
            if (m != null)
                m.security.add(Map.of(securitySchemaName, List.of()));
        };
        this.RESTentitiesPaths.forEach(p -> {
            secureLambda.accept(p.get);
            secureLambda.accept(p.post);
            secureLambda.accept(p.put);
            secureLambda.accept(p.delete);
        });
    }

    public void addInfo(Info info) {
        this.info = info;
    }

    static class Tag {

        public String name;
        public String description;

        private Tag(String name, String description) {
            this.name = name;
            this.description = description;
        }

        public static Tag from(String name, String description) {
            return new Tag(name, description);
        }
    }

    public static class Path {
        public String summary;
        public String description;
        public List<Object> parameters;
        public Method get;
        public Method put;
        public Method post;
        public Method delete;
    }

    public static class Method {
        public String summary;
        public List<String> tags;
        public RequestBody requestBody;
        public Map<String, Object> responses = new LinkedHashMap<>();
        public List<Object> parameters;
        public List<Map<String, Object>> security = new ArrayList<>();
    }

    public static class RequestBody {
        public String description;
        public boolean required;
        public Map<String, Object> content;
    }

    public static class Response {
        public String description;
        public Map<String, Object> content;
    }

    public static class Server {
        public String url;
        public String description;

        private Server(String url, String description) {
            this.url = url;
            this.description = description;
        }

        public static Server from(String url, String description) {
            return new Server(url, description);
        }
    }

    public static class Schema {
        public String type;
        public Map<String, Object> properties;
        public List<String> required;
    }

    public static class SchemaProperty {
        public String type;
        public String format;
        public SchemaProperty items;
        public String $ref;
        @JsonProperty("enum")
        public List<Object> _enum;
    }

    public static class Parameter {
        public String name;
        public String in;
        public String description;
        public boolean required;
        public SchemaProperty schema;
        public boolean allowReserver;
        public Map<String, Object> examples;
    }

    public static class SecuritySchema {
        public String type;
        public String description;
        public Map<String, Map<String, Object>> flows;
    }

    public static class Info {
        public String title;
        public String version;
        public String description;
        public String termsOfService;
        public Contact contact;
        public License license;
        public ExternalDocs externalDocs;
    }

    public static class Contact {
        public String name;
        public String email;
        public String url;
    }

    public static class License {
        public String name;
        public String url;
    }

    public static class ExternalDocs {
        public String description;
        public String url;
    }

    public enum SchemaType {
        ENTITY,
        ENTITY_WITH_META,
        ENTITY_LK
    }
}
